diff options
Diffstat (limited to 'app/[lng]/sales')
51 files changed, 0 insertions, 3325 deletions
diff --git a/app/[lng]/sales/(sales)/bid-projects/page.tsx b/app/[lng]/sales/(sales)/bid-projects/page.tsx deleted file mode 100644 index 38cbf91a..00000000 --- a/app/[lng]/sales/(sales)/bid-projects/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getBidProjectLists } from "@/lib/bidding-projects/service" -import { searchParamsBidProjectsCache } from "@/lib/bidding-projects/validation" -import { BidProjectsTable } from "@/lib/bidding-projects/table/projects-table" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsBidProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getBidProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 견적 프로젝트 관리 - </h2> - {/* <p className="text-muted-foreground"> - SAP(S-ERP)로부터 수신한 견적 프로젝트 데이터입니다. 기술영업의 Budgetary RFQ에서 사용됩니다. - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <BidProjectsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/sales/(sales)/bqcbe/page.tsx b/app/[lng]/sales/(sales)/bqcbe/page.tsx deleted file mode 100644 index 30935645..00000000 --- a/app/[lng]/sales/(sales)/bqcbe/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllCBE } from "@/lib/rfqs/service" -import { searchParamsCBECache } from "@/lib/rfqs/validations" - -import { AllCbeTable } from "@/lib/cbe/table/cbe-table" - -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - } - searchParams: Promise<SearchParams> - rfqType: RfqType -} - -export default async function RfqCBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getAllCBE({ - ...search, - filters: validFilters, - rfqType - } - ) - ]) - - // 4) 렌더링 - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - CBE 관리 - </h2> - {/* <p className="text-muted-foreground"> - 초대된 협력업체에게 CBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <AllCbeTable promises={promises}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/bqtbe/page.tsx b/app/[lng]/sales/(sales)/bqtbe/page.tsx deleted file mode 100644 index 3e56cfaa..00000000 --- a/app/[lng]/sales/(sales)/bqtbe/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { AllTbeTable } from "@/lib/tbe/table/tbe-table" -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - } - searchParams: Promise<SearchParams> - rfqType: RfqType -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getAllTBE({ - ...search, - filters: validFilters, - rfqType - } - ) - ]) - - // 4) 렌더링 - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - TBE 관리 - </h2> - {/* <p className="text-muted-foreground"> - 초대된 협력업체에게 TBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <AllTbeTable promises={promises}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/cbe/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getCBE, getTBE } from "@/lib/rfqs/service" -import { searchParamsCBECache, } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" -import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getCBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - Commercial Bid Evaluation - </h3> - <p className="text-sm text-muted-foreground"> - 초대된 협력업체에게 CBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> - </div> - <Separator /> - <div> - <CbeTable promises={promises} rfqId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx deleted file mode 100644 index 2b80e64f..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/layout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { ArrowLeft } from "lucide-react" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" - -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function RfqLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string, id: string } -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Matched Vendors", - href: `/${lng}/evcp/budgetary/${id}`, - }, - { - title: "TBE", - href: `/${lng}/evcp/budgetary/${id}/tbe`, - }, - { - title: "CBE", - href: `/${lng}/evcp/budgetary/${id}/cbe`, - }, - - ] - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - <div className="flex items-center justify-end mb-4"> - <Link href={`/${lng}/evcp/budgetary-rfq`} passHref> - <Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto"> - <ArrowLeft className="mr-1 h-4 w-4" /> - <span>Budgetary RFQ 목록으로 돌아가기</span> - </Button> - </Link> - </div> - <div className="space-y-0.5"> - {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} - <h2 className="text-2xl font-bold tracking-tight"> - {rfq - ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` - : "Loading RFQ..."} - </h2> - - <p className="text-muted-foreground"> - {rfq - ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` - : ""} - </p> - <h3>Due Date:{rfq && rfq?.dueDate && <strong>{formatDate(rfq?.dueDate, "KR")}</strong>}</h3> - </div> - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="lg:w-64 flex-shrink-0"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="lg:w-[calc(100%-16rem)] overflow-auto">{children}</div> - </div> - </div> - </section> - </div> - </> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getMatchedVendors } from "@/lib/rfqs/service" -import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" -import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" -import { RfqType } from "@/lib/rfqs/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> - rfqType: RfqType -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - const searchParams = await props.searchParams - const search = searchParamsMatchedVCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getMatchedVendors({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - Vendors - </h3> - <p className="text-sm text-muted-foreground"> - 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다. <br/>"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> - </div> - <Separator /> - <div> - <MatchedVendorsTable promises={promises} rfqId={idAsNumber} rfqType={rfqType}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/[id]/tbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - Technical Bid Evaluation - </h3> - <p className="text-sm text-muted-foreground"> - 초대된 협력업체에게 TBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> - </div> - <Separator /> - <div> - <TbeTable promises={promises} rfqId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx b/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx deleted file mode 100644 index f342bbff..00000000 --- a/app/[lng]/sales/(sales)/budgetary-rfq/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/rfqs/validations" -import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" -import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" -import { getAllItems } from "@/lib/items/service" -import { RfqType } from "@/lib/rfqs/validations" -import { Ellipsis } from "lucide-react" - -interface RfqPageProps { - searchParams: Promise<SearchParams>; - rfqType: RfqType; - title: string; - description: string; -} - -export default async function RfqPage({ - searchParams, - rfqType = RfqType.PURCHASE_BUDGETARY, - title = "Budgetary Quote", - description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - {title} - </h2> - {/* <p className="text-muted-foreground"> - {description} - 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <RfqsTable promises={promises} rfqType={rfqType} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx deleted file mode 100644 index b1be29db..00000000 --- a/app/[lng]/sales/(sales)/budgetary-tech-sales-hull/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { searchParamsHullCache } from "@/lib/techsales-rfq/validations" -import { getTechSalesHullRfqsWithJoin } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface HullRfqPageProps { - searchParams: Promise<SearchParams> -} - -export default async function HullRfqPage(props: HullRfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 해양 HULL용 파라미터 파싱 - const search = searchParamsHullCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // 기술영업 해양 Hull RFQ 데이터를 Promise.all로 감싸서 전달 - const promises = Promise.all([ - getTechSalesHullRfqsWithJoin({ - ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) - filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) - }) - ]) - - return ( - <Shell variant="fullscreen" className="h-full"> {/* fullscreen variant 사용 */} - {/* 고정 헤더 영역 */} - <div className="flex-shrink-0"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 기술영업-해양 Hull RFQ - </h2> - </div> - </div> - </div> - - {/* 테이블 영역 - 남은 공간 모두 차지 */} - <div className="flex-1 min-h-0"> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={2} - filterableColumnCount={3} - cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]} - shrinkZero - /> - } - > - <RFQListTable promises={promises} className="h-full" rfqType="HULL" /> - </React.Suspense> - </div> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx deleted file mode 100644 index b7bf9d15..00000000 --- a/app/[lng]/sales/(sales)/budgetary-tech-sales-ship/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { searchParamsShipCache } from "@/lib/techsales-rfq/validations" -import { getTechSalesShipRfqsWithJoin } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface RfqPageProps { - searchParams: Promise<SearchParams> -} - -export default async function RfqPage(props: RfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 조선용 파라미터 파싱 - const search = searchParamsShipCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // 기술영업 조선 RFQ 데이터를 Promise.all로 감싸서 전달 - const promises = Promise.all([ - getTechSalesShipRfqsWithJoin({ - ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) - filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) - }) - ]) - - return ( - <Shell variant="fullscreen" className="h-full"> {/* fullscreen variant 사용 */} - {/* 고정 헤더 영역 */} - <div className="flex-shrink-0"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 기술영업-조선 RFQ - </h2> - </div> - </div> - </div> - - {/* 테이블 영역 - 남은 공간 모두 차지 */} - <div className="flex-1 min-h-0"> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={2} - filterableColumnCount={3} - cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]} - shrinkZero - /> - } - > - <RFQListTable promises={promises} className="h-full" rfqType="SHIP" /> - </React.Suspense> - </div> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx b/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx deleted file mode 100644 index f84a9794..00000000 --- a/app/[lng]/sales/(sales)/budgetary-tech-sales-top/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { searchParamsTopCache } from "@/lib/techsales-rfq/validations" -import { getTechSalesTopRfqsWithJoin } from "@/lib/techsales-rfq/service" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { RFQListTable } from "@/lib/techsales-rfq/table/rfq-table" -import { type SearchParams } from "@/types/table" -import * as React from "react" - -interface HullRfqPageProps { - searchParams: Promise<SearchParams> -} - -export default async function HullRfqPage(props: HullRfqPageProps) { - // searchParams를 await하여 resolve - const searchParams = await props.searchParams - - // 해양 TOP용 파라미터 파싱 - const search = searchParamsTopCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - // 기술영업 해양 TOP RFQ 데이터를 Promise.all로 감싸서 전달 - const promises = Promise.all([ - getTechSalesTopRfqsWithJoin({ - ...search, // 모든 파라미터 전달 (page, perPage, sort, basicFilters, filters 등) - filters: validFilters, // 고급 필터를 명시적으로 오버라이드 (파싱된 버전) - }) - ]) - - return ( - <Shell variant="fullscreen" className="h-full"> {/* fullscreen variant 사용 */} - {/* 고정 헤더 영역 */} - <div className="flex-shrink-0"> - <div className="flex items-center justify-between"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 기술영업-해양 TOP RFQ - </h2> - </div> - </div> - </div> - - {/* 테이블 영역 - 남은 공간 모두 차지 */} - <div className="flex-1 min-h-0"> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={2} - filterableColumnCount={3} - cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem", "8rem", "10rem", "8rem"]} - shrinkZero - /> - } - > - <RFQListTable promises={promises} className="h-full" rfqType="TOP" /> - </React.Suspense> - </div> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx deleted file mode 100644 index 956facd3..00000000 --- a/app/[lng]/sales/(sales)/budgetary/[id]/cbe/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getCBE, getTBE } from "@/lib/rfqs/service" -import { searchParamsCBECache, } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" -import { CbeTable } from "@/lib/rfqs/cbe-table/cbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsCBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getCBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - Commercial Bid Evaluation - </h3> - <p className="text-sm text-muted-foreground"> - 초대된 협력업체에게 CBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 CBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> - </div> - <Separator /> - <div> - <CbeTable promises={promises} rfqId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx deleted file mode 100644 index d58d8363..00000000 --- a/app/[lng]/sales/(sales)/budgetary/[id]/layout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Metadata } from "next" -import Link from "next/link" -import { ArrowLeft } from "lucide-react" -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" -import { RfqViewWithItems } from "@/db/schema/rfq" -import { findRfqById } from "@/lib/rfqs/service" -import { formatDate } from "@/lib/utils" -import { Button } from "@/components/ui/button" - -export const metadata: Metadata = { - title: "Vendor Detail", -} - -export default async function RfqLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string, id: string } -}) { - - // 1) URL 파라미터에서 id 추출, Number로 변환 - const resolvedParams = await params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - // 2) DB에서 해당 협력업체 정보 조회 - const rfq: RfqViewWithItems | null = await findRfqById(idAsNumber) - - // 3) 사이드바 메뉴 - const sidebarNavItems = [ - { - title: "Matched Vendors", - href: `/${lng}/evcp/budgetary/${id}`, - }, - { - title: "TBE", - href: `/${lng}/evcp/budgetary/${id}/tbe`, - }, - { - title: "CBE", - href: `/${lng}/evcp/budgetary/${id}/cbe`, - }, - ] - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - {/* RFQ 목록으로 돌아가는 링크 추가 */} - <div className="flex items-center justify-end mb-4"> - <Link href={`/${lng}/evcp/budgetary`} passHref> - <Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto"> - <ArrowLeft className="mr-1 h-4 w-4" /> - <span>Budgetary Quote 목록으로 돌아가기</span> - </Button> - </Link> - </div> - - <div className="space-y-0.5"> - {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} - <h2 className="text-2xl font-bold tracking-tight"> - {rfq - ? `${rfq.projectCode ?? ""} ${rfq.rfqCode ?? ""} 관리` - : "Loading RFQ..."} - </h2> - - <p className="text-muted-foreground"> - {rfq - ? `${rfq.description ?? ""} ${rfq.lines.map(line => line.itemCode).join(", ")}` - : ""} - </p> - <h3>Due Date:{rfq && rfq?.dueDate && <strong>{formatDate(rfq?.dueDate, "KR")}</strong>}</h3> - </div> - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="lg:w-64 flex-shrink-0"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="lg:w-[calc(100%-16rem)] overflow-auto">{children}</div> - </div> - </div> - </section> - </div> - </> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx deleted file mode 100644 index dd9df563..00000000 --- a/app/[lng]/sales/(sales)/budgetary/[id]/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getMatchedVendors } from "@/lib/rfqs/service" -import { searchParamsMatchedVCache } from "@/lib/rfqs/validations" -import { MatchedVendorsTable } from "@/lib/rfqs/vendor-table/vendors-table" -import { RfqType } from "@/lib/rfqs/validations" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> - rfqType: RfqType -} - -export default async function RfqPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - const rfqType = props.rfqType ?? RfqType.BUDGETARY // rfqType이 없으면 BUDGETARY로 설정 - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - const searchParams = await props.searchParams - const search = searchParamsMatchedVCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getMatchedVendors({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - Vendors - </h3> - <p className="text-sm text-muted-foreground"> - 등록된 협력업체 중에서 이 RFQ 아이템에 매칭되는 업체를 보여줍니다. <br/>"발행하기" 버튼을 통해 RFQ를 전송하면 첨부파일과 함께 RFQ 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> - </div> - <Separator /> - <div> - <MatchedVendorsTable promises={promises} rfqId={idAsNumber} rfqType={rfqType}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx b/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx deleted file mode 100644 index ec894e1c..00000000 --- a/app/[lng]/sales/(sales)/budgetary/[id]/tbe/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { TbeTable } from "@/lib/rfqs/tbe-table/tbe-table" - -interface IndexPageProps { - // Next.js 13 App Router에서 기본으로 주어지는 객체들 - params: { - lng: string - id: string - } - searchParams: Promise<SearchParams> -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - const id = resolvedParams.id - - const idAsNumber = Number(id) - - // 2) SearchParams 파싱 (Zod) - // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getTBE({ - ...search, - filters: validFilters, - }, - idAsNumber) - ]) - - // 4) 렌더링 - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium"> - Technical Bid Evaluation - </h3> - <p className="text-sm text-muted-foreground"> - 초대된 협력업체에게 TBE를 보낼 수 있습니다. <br/>체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> - </div> - <Separator /> - <div> - <TbeTable promises={promises} rfqId={idAsNumber}/> - </div> - </div> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/budgetary/page.tsx b/app/[lng]/sales/(sales)/budgetary/page.tsx deleted file mode 100644 index 15b4cdd4..00000000 --- a/app/[lng]/sales/(sales)/budgetary/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { searchParamsCache } from "@/lib/rfqs/validations" -import { getRfqs, getRfqStatusCounts } from "@/lib/rfqs/service" -import { RfqsTable } from "@/lib/rfqs/table/rfqs-table" -import { getAllItems } from "@/lib/items/service" -import { RfqType } from "@/lib/rfqs/validations" -import { Ellipsis } from "lucide-react" - -interface RfqPageProps { - searchParams: Promise<SearchParams>; - rfqType: RfqType; - title: string; - description: string; -} - -export default async function RfqPage({ - searchParams, - rfqType = RfqType.BUDGETARY, - title = "Budgetary Quote", - description = "Budgetary Quote를 등록하여 요청 및 응답을 관리할 수 있습니다." -}: RfqPageProps) { - const search = searchParamsCache.parse(await searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRfqs({ - ...search, - filters: validFilters, - rfqType // 전달받은 rfqType 사용 - }), - getRfqStatusCounts(rfqType), // rfqType 전달 - getAllItems() - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - {title} - </h2> - {/* <p className="text-muted-foreground"> - {description} - 기본적인 정보와 RFQ를 위한 아이템 등록 및 첨부를 한 후, - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> 을 클릭하면 "Proceed"를 통해 상세화면으로 이동하여 진행할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <RfqsTable promises={promises} rfqType={rfqType} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/dashboard/page.tsx b/app/[lng]/sales/(sales)/dashboard/page.tsx deleted file mode 100644 index 1d61dc16..00000000 --- a/app/[lng]/sales/(sales)/dashboard/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// app/invalid-access/page.tsx - -export default function InvalidAccessPage() { - return ( - <main style={{ padding: '40px', textAlign: 'center' }}> - <h1>부적절한 접근입니다</h1> - <p> - 협력업체(Vendor)가 EVCP 화면에 접속하거나 <br /> - SHI 계정이 협력업체 화면에 접속하려고 시도하는 경우입니다. - </p> - <p> - <strong>접근 권한이 없으므로, 다른 화면으로 이동해 주세요.</strong> - </p> - </main> - ); - } -
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/esg-check-list/page.tsx b/app/[lng]/sales/(sales)/esg-check-list/page.tsx deleted file mode 100644 index dd97c74c..00000000 --- a/app/[lng]/sales/(sales)/esg-check-list/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getEsgEvaluations } from "@/lib/esg-check-list/service" -import { getEsgEvaluationsSchema } from "@/lib/esg-check-list/validation" -import { EsgEvaluationsTable } from "@/lib/esg-check-list/table/esg-table" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = getEsgEvaluationsSchema.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getEsgEvaluations({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - ESG 자가진단평가 문항 관리 - </h2> - {/* <p className="text-muted-foreground"> - 협력업체 평가에 사용되는 ESG 자가진단표를 관리{" "} - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <EsgEvaluationsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx b/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx deleted file mode 100644 index 34409524..00000000 --- a/app/[lng]/sales/(sales)/evaluation-check-list/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* IMPORT */
-import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton';
-import { getRegEvalCriteria } from '@/lib/evaluation-criteria/service';
-import { getValidFilters } from '@/lib/data-table';
-import RegEvalCriteriaTable from '@/lib/evaluation-criteria/table/reg-eval-criteria-table';
-import { searchParamsCache } from '@/lib/evaluation-criteria/validations';
-import { Shell } from '@/components/shell';
-import { Skeleton } from '@/components/ui/skeleton';
-import { Suspense } from 'react';
-import { type SearchParams } from '@/types/table';
-
-// ----------------------------------------------------------------------------------------------------
-
-/* TYPES */
-interface EvaluationCriteriaPageProps {
- searchParams: Promise<SearchParams>
-}
-
-// ----------------------------------------------------------------------------------------------------
-
-/* REGULAR EVALUATION CRITERIA PAGE */
-async function EvaluationCriteriaPage(props: EvaluationCriteriaPageProps) {
- const searchParams = await props.searchParams;
- const search = searchParamsCache.parse(searchParams);
- const validFilters = getValidFilters(search.filters);
- const promises = Promise.all([
- getRegEvalCriteria({
- ...search,
- filters: validFilters,
- }),
- ]);
-
- return (
- <Shell className="gap-2">
- <div className="flex items-center justify-between space-y-2">
- <div className="flex items-center justify-between space-y-2">
- <div>
- <h2 className="text-2xl font-bold tracking-tight">
- 협력업체 평가기준표 관리
- </h2>
- {/* <p className="text-muted-foreground">
- 협력업체 평가에 사용되는 평가기준표를 관리{" "}
- <span className="inline-flex items-center whitespace-nowrap">
- <Ellipsis className="size-3" />
- <span className="ml-1">버튼</span>
- </span>
- 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다.
- </p> */}
- </div>
- </div>
- </div>
-
- <Suspense fallback={<Skeleton className="h-7 w-52" />}>
- {/* <DateRangePicker
- triggerSize="sm"
- triggerClassName="ml-auto w-56 sm:w-60"
- align="end"
- shallow={false}
- /> */}
- </Suspense>
- <Suspense
- fallback={
- <DataTableSkeleton
- columnCount={11}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <RegEvalCriteriaTable promises={promises} />
- </Suspense>
- </Shell>
- )
-}
-
-// ----------------------------------------------------------------------------------------------------
-
-/* EXPORT */
-export default EvaluationCriteriaPage;
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx b/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx deleted file mode 100644 index 56b8ecef..00000000 --- a/app/[lng]/sales/(sales)/evaluation-target-list/page.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" - -import { getDefaultEvaluationYear, searchParamsEvaluationTargetsCache } from "@/lib/evaluation-target-list/validation" -import { getEvaluationTargets } from "@/lib/evaluation-target-list/service" -import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table" - -export const metadata: Metadata = { - title: "협력업체 평가 대상 관리", - description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.", -} - -interface EvaluationTargetsPageProps { - searchParams: Promise<SearchParams> -} - - - -export default async function EvaluationTargetsPage(props: EvaluationTargetsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationTargetsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 기본 필터 처리 (통일된 이름 사용) - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - console.log("Using search.basicFilters:", basicFilters); - } else { - console.log("No basic filters found"); - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자도 통일된 이름 사용 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // 현재 평가년도 (필터에서 가져오거나 기본값 사용) - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getEvaluationTargets({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - return ( - <Shell className="gap-4"> - {/* 간소화된 헤더 */} - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 평가 대상 관리 - </h2> - <Badge variant="outline" className="text-sm"> - {currentEvaluationYear}년도 - </Badge> - - </div> - </div> - </div> - - {/* 메인 테이블 (통계는 테이블 내부로 이동) */} - <React.Suspense - key={JSON.stringify(searchParams)} // URL 파라미터가 변경될 때마다 강제 리렌더링 - fallback={ - <DataTableSkeleton - columnCount={12} - searchableColumnCount={2} - filterableColumnCount={6} - cellWidths={[ - "3rem", // checkbox - "5rem", // 평가년도 - "4rem", // 구분 - "8rem", // 벤더코드 - "12rem", // 벤더명 - "4rem", // 내외자 - "6rem", // 자재구분 - "5rem", // 상태 - "5rem", // 의견일치 - "8rem", // 담당자현황 - "10rem", // 관리자의견 - "8rem" // actions - ]} - shrinkZero - /> - } - > - {currentEvaluationYear && - <EvaluationTargetsTable - promises={promises} - evaluationYear={currentEvaluationYear} - /> -} - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/evaluation/page.tsx b/app/[lng]/sales/(sales)/evaluation/page.tsx deleted file mode 100644 index 2d8cbed7..00000000 --- a/app/[lng]/sales/(sales)/evaluation/page.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// ================================================================ -// 4. PERIODIC EVALUATIONS PAGE -// ================================================================ - -import * as React from "react" -import { Metadata } from "next" -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { HelpCircle } from "lucide-react" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { PeriodicEvaluationsTable } from "@/lib/evaluation/table/evaluation-table" -import { getPeriodicEvaluations } from "@/lib/evaluation/service" -import { searchParamsEvaluationsCache } from "@/lib/evaluation/validation" - -export const metadata: Metadata = { - title: "협력업체 정기평가", - description: "협력업체 정기평가 진행 현황을 관리합니다.", -} - -interface PeriodicEvaluationsPageProps { - searchParams: Promise<SearchParams> -} - -// 프로세스 안내 팝오버 컴포넌트 -function ProcessGuidePopover() { - return ( - <Popover> - <PopoverTrigger asChild> - <Button variant="ghost" size="icon" className="h-6 w-6"> - <HelpCircle className="h-4 w-4 text-muted-foreground" /> - </Button> - </PopoverTrigger> - <PopoverContent className="w-96" align="start"> - <div className="space-y-3"> - <div className="space-y-1"> - <h4 className="font-medium">정기평가 프로세스</h4> - {/* <p className="text-sm text-muted-foreground"> - 확정된 평가 대상 업체들에 대한 정기평가 절차입니다. - </p> */} - </div> - <div className="space-y-3 text-sm"> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 1 - </div> - <div> - <p className="font-medium">평가 대상 확정</p> - <p className="text-muted-foreground">평가 대상으로 확정된 업체들의 정기평가가 자동 생성됩니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 2 - </div> - <div> - <p className="font-medium">업체 자료 제출</p> - <p className="text-muted-foreground">각 업체는 평가에 필요한 자료를 제출 마감일까지 제출해야 합니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 3 - </div> - <div> - <p className="font-medium">평가자 검토</p> - <p className="text-muted-foreground">지정된 평가자들이 평가표를 기반으로 점수를 매기고 검토합니다.</p> - </div> - </div> - <div className="flex gap-3"> - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> - 4 - </div> - <div> - <p className="font-medium">최종 확정</p> - <p className="text-muted-foreground">모든 평가가 완료되면 최종 점수와 등급이 확정됩니다.</p> - </div> - </div> - </div> - </div> - </PopoverContent> - </Popover> - ) -} - -// TODO: 이 함수들은 실제 서비스 파일에서 구현해야 함 -function getDefaultEvaluationYear() { - return new Date().getFullYear() -} - - - -export default async function PeriodicEvaluationsPage(props: PeriodicEvaluationsPageProps) { - const searchParams = await props.searchParams - const search = searchParamsEvaluationsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters || []) - - // 기본 필터 처리 - let basicFilters = [] - if (search.basicFilters && search.basicFilters.length > 0) { - basicFilters = search.basicFilters - } - - // 모든 필터를 합쳐서 처리 - const allFilters = [...validFilters, ...basicFilters] - - // 조인 연산자 - const joinOperator = search.basicJoinOperator || search.joinOperator || 'and'; - - // 현재 평가년도 - const currentEvaluationYear = search.evaluationYear || getDefaultEvaluationYear() - - // Promise.all로 감싸서 전달 - const promises = Promise.all([ - getPeriodicEvaluations({ - ...search, - filters: allFilters, - joinOperator, - }) - ]) - - return ( - <Shell className="gap-4"> - {/* 헤더 */} - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 협력업체 정기평가 - </h2> - <Badge variant="outline" className="text-sm"> - {currentEvaluationYear}년도 - </Badge> - </div> - </div> - </div> - - {/* 메인 테이블 */} - <React.Suspense - key={JSON.stringify(searchParams)} - fallback={ - <DataTableSkeleton - columnCount={15} - searchableColumnCount={2} - filterableColumnCount={8} - cellWidths={[ - "3rem", // checkbox - "5rem", // 평가년도 - "5rem", // 평가기간 - "4rem", // 구분 - "8rem", // 벤더코드 - "12rem", // 벤더명 - "4rem", // 내외자 - "6rem", // 자재구분 - "5rem", // 문서제출 - "4rem", // 제출일 - "4rem", // 마감일 - "4rem", // 총점 - "4rem", // 등급 - "5rem", // 진행상태 - "8rem" // actions - ]} - shrinkZero - /> - } - > - <PeriodicEvaluationsTable - promises={promises} - evaluationYear={currentEvaluationYear} - /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/manage/actions.ts b/app/[lng]/sales/(sales)/faq/manage/actions.ts deleted file mode 100644 index bc443a8a..00000000 --- a/app/[lng]/sales/(sales)/faq/manage/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use server';
-
-import { promises as fs } from 'fs';
-import path from 'path';
-import { FaqCategory } from '@/components/faq/FaqCard';
-import { fallbackLng } from '@/i18n/settings';
-
-const FAQ_CONFIG_PATH = path.join(process.cwd(), 'config', 'faqDataConfig.ts');
-
-export async function updateFaqData(lng: string, newData: FaqCategory[]) {
- try {
- const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
- const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
- if (!dataMatch) {
- throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
- }
-
- const allData = eval(`(${dataMatch[1]})`);
- const updatedData = {
- ...allData,
- [lng]: newData
- };
-
- const newFileContent = `import { FaqCategory } from "@/components/faq/FaqCard";\n\ninterface LocalizedFaqCategories {\n [lng: string]: FaqCategory[];\n}\n\nexport const faqCategories: LocalizedFaqCategories = ${JSON.stringify(updatedData, null, 4)};`;
- await fs.writeFile(FAQ_CONFIG_PATH, newFileContent, 'utf-8');
-
- return { success: true };
- } catch (error) {
- console.error('FAQ 데이터 업데이트 중 오류 발생:', error);
- return { success: false, error: '데이터 업데이트 중 오류가 발생했습니다.' };
- }
-}
-
-export async function getFaqData(lng: string): Promise<{ data: FaqCategory[] }> {
- try {
- const fileContent = await fs.readFile(FAQ_CONFIG_PATH, 'utf-8');
- const dataMatch = fileContent.match(/export const faqCategories[^=]*=\s*(\{[\s\S]*\});/);
- if (!dataMatch) {
- throw new Error('FAQ 데이터 형식이 올바르지 않습니다.');
- }
-
- const allData = eval(`(${dataMatch[1]})`);
- return { data: allData[lng] || allData[fallbackLng] || [] };
- } catch (error) {
- console.error('FAQ 데이터 읽기 중 오류 발생:', error);
- return { data: [] };
- }
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/manage/page.tsx b/app/[lng]/sales/(sales)/faq/manage/page.tsx deleted file mode 100644 index 011bbfa4..00000000 --- a/app/[lng]/sales/(sales)/faq/manage/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FaqManager } from '@/components/faq/FaqManager';
-import { getFaqData, updateFaqData } from './actions';
-import { revalidatePath } from 'next/cache';
-import { FaqCategory } from '@/components/faq/FaqCard';
-
-interface Props {
- params: {
- lng: string;
- }
-}
-
-export default async function FaqManagePage(props: Props) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
- const { data } = await getFaqData(lng);
-
- async function handleSave(newData: FaqCategory[]) {
- 'use server';
- await updateFaqData(lng, newData);
- revalidatePath(`/${lng}/evcp/faq`);
- }
-
- return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="space-y-6 p-10 pb-16">
- <div className="space-y-0.5">
- <h2 className="text-2xl font-bold tracking-tight">FAQ Management</h2>
- <p className="text-muted-foreground">
- Manage FAQ categories and items for {lng.toUpperCase()} language.
- </p>
- </div>
- <FaqManager initialData={data} onSave={handleSave} lng={lng} />
- </div>
- </section>
- </div>
- );
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/faq/page.tsx b/app/[lng]/sales/(sales)/faq/page.tsx deleted file mode 100644 index 00956591..00000000 --- a/app/[lng]/sales/(sales)/faq/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Separator } from "@/components/ui/separator"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { faqCategories } from "@/config/faqDataConfig"
-import { FaqCard } from "@/components/faq/FaqCard"
-import { Button } from "@/components/ui/button"
-import { Settings } from "lucide-react"
-import Link from "next/link"
-import { fallbackLng } from "@/i18n/settings"
-
-interface Props {
- params: {
- lng: string;
- }
-}
-
-export default async function FaqPage(props: Props) {
- const resolvedParams = await props.params
- const lng = resolvedParams.lng
- const localizedFaqCategories = faqCategories[lng] || faqCategories[fallbackLng];
-
- return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="space-y-6 p-10 pb-16">
- <div className="flex justify-between items-center">
- <div className="space-y-0.5">
- <h2 className="text-2xl font-bold tracking-tight">FAQ</h2>
- {/* <p className="text-muted-foreground">
- Find answers to common questions about using the EVCP system.
- </p> */}
- </div>
- <Link href={`/${lng}/evcp/faq/manage`}>
- <Button variant="outline">
- <Settings className="w-4 h-4 mr-2" />
- FAQ 관리
- </Button>
- </Link>
- </div>
- <Separator className="my-6" />
-
- <Tabs defaultValue={localizedFaqCategories[0]?.label} className="space-y-4">
- <TabsList>
- {localizedFaqCategories.map((category) => (
- <TabsTrigger key={category.label} value={category.label}>
- {category.label}
- </TabsTrigger>
- ))}
- </TabsList>
-
- {localizedFaqCategories.map((category) => (
- <TabsContent key={category.label} value={category.label} className="space-y-4">
- {category.items.map((item, index) => (
- <FaqCard key={index} item={item} />
- ))}
- </TabsContent>
- ))}
- </Tabs>
- </div>
- </section>
- </div>
- )
-}
diff --git a/app/[lng]/sales/(sales)/items-tech/layout.tsx b/app/[lng]/sales/(sales)/items-tech/layout.tsx deleted file mode 100644 index d375059b..00000000 --- a/app/[lng]/sales/(sales)/items-tech/layout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from "react"
-import { ItemTechContainer } from "@/components/items-tech/item-tech-container"
-import { Shell } from "@/components/shell"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-
-// Layout 컴포넌트는 서버 컴포넌트입니다
-export default function ItemsShipLayout({
- children,
-}: {
- children: React.ReactNode
-}) {
- // 아이템 타입 정의
- const itemTypes = [
- { id: "ship", name: "조선 아이템" },
- { id: "top", name: "해양 TOP" },
- { id: "hull", name: "해양 HULL" },
- ]
-
- return (
- <Shell className="gap-4">
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={6}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <ItemTechContainer itemTypes={itemTypes}>
- {children}
- </ItemTechContainer>
- </React.Suspense>
- </Shell>
- )
-}
diff --git a/app/[lng]/sales/(sales)/items-tech/page.tsx b/app/[lng]/sales/(sales)/items-tech/page.tsx deleted file mode 100644 index 55ac9c63..00000000 --- a/app/[lng]/sales/(sales)/items-tech/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from "react"
-import { type SearchParams } from "@/types/table"
-
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { shipbuildingSearchParamsCache, offshoreTopSearchParamsCache, offshoreHullSearchParamsCache } from "@/lib/items-tech/validations"
-import { getShipbuildingItems, getOffshoreTopItems, getOffshoreHullItems } from "@/lib/items-tech/service"
-import { OffshoreTopTable } from "@/lib/items-tech/table/top/offshore-top-table"
-import { OffshoreHullTable } from "@/lib/items-tech/table/hull/offshore-hull-table"
-
-// 대소문자 문제 해결 - 실제 파일명에 맞게 import
-import { ItemsShipTable } from "@/lib/items-tech/table/ship/Items-ship-table"
-
-interface IndexPageProps {
- searchParams: Promise<SearchParams>
-}
-
-export default async function IndexPage({ searchParams }: IndexPageProps) {
- const params = await searchParams
- const shipbuildingSearch = shipbuildingSearchParamsCache.parse(params)
- const offshoreTopSearch = offshoreTopSearchParamsCache.parse(params)
- const offshoreHullSearch = offshoreHullSearchParamsCache.parse(params)
- const validShipbuildingFilters = getValidFilters(shipbuildingSearch.filters || [])
- const validOffshoreTopFilters = getValidFilters(offshoreTopSearch.filters || [])
- const validOffshoreHullFilters = getValidFilters(offshoreHullSearch.filters || [])
-
-
- // URL에서 아이템 타입 가져오기
- const itemType = params.type || "ship"
-
- return (
- <div>
- {itemType === "ship" && (
- <ItemsShipTable
- promises={Promise.all([
- getShipbuildingItems({
- ...shipbuildingSearch,
- filters: validShipbuildingFilters,
- }),
- ]).then(([result]) => result)}
- />
- )}
-
- {itemType === "top" && (
- <OffshoreTopTable
- promises={Promise.all([
- getOffshoreTopItems({
- ...offshoreTopSearch,
- filters: validOffshoreTopFilters,
- }),
- ]).then(([result]) => result)}
- />
- )}
-
- {itemType === "hull" && (
- <OffshoreHullTable
- promises={Promise.all([
- getOffshoreHullItems({
- ...offshoreHullSearch,
- filters: validOffshoreHullFilters,
- }),
- ]).then(([result]) => result)}
- />
- )}
- </div>
- )
-}
diff --git a/app/[lng]/sales/(sales)/items/page.tsx b/app/[lng]/sales/(sales)/items/page.tsx deleted file mode 100644 index f8d9a5b1..00000000 --- a/app/[lng]/sales/(sales)/items/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// app/items/page.tsx (업데이트) -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { searchParamsCache } from "@/lib/items/validations" -import { getItems } from "@/lib/items/service" -import { ItemsTable } from "@/lib/items/table/items-table" -import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - // pageSize 기반으로 모드 자동 결정 - const isInfiniteMode = search.perPage >= 1_000_000 - - // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 - // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 - const promises = isInfiniteMode - ? undefined - : Promise.all([ - getItems(search), // searchParamsCache의 결과를 그대로 사용 - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 패키지 넘버 - </h2> - {/* <p className="text-muted-foreground"> - S-EDP로부터 수신된 패키지 정보이며 PR 전 입찰, 견적에 사용되며 벤더 데이터, 문서와 연결됩니다. - </p> */} - </div> - </div> - - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* DateRangePicker 등 추가 컴포넌트 */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - {/* 통합된 ItemsTable 컴포넌트 사용 */} - <ItemsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/layout.tsx b/app/[lng]/sales/(sales)/layout.tsx deleted file mode 100644 index 82b53307..00000000 --- a/app/[lng]/sales/(sales)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from 'react'; -import { Header } from '@/components/layout/Header'; -import { SiteFooter } from '@/components/layout/Footer'; - -export default function EvcpLayout({ children }: { children: ReactNode }) { - return ( - <div className="relative flex min-h-svh flex-col bg-background"> - {/* <div className="relative flex min-h-svh flex-col bg-slate-100 "> */} - <Header /> - <main className="flex flex-1 flex-col"> - <div className='container-wrapper'> - {children} - </div> - </main> - <SiteFooter/> - </div> - ); -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/project-gtc/page.tsx b/app/[lng]/sales/(sales)/project-gtc/page.tsx deleted file mode 100644 index d5cb467a..00000000 --- a/app/[lng]/sales/(sales)/project-gtc/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { getProjectGtcList } from "@/lib/project-gtc/service" -import { projectGtcSearchParamsSchema } from "@/lib/project-gtc/validations" -import { ProjectGtcTable } from "@/lib/project-gtc/table/project-gtc-table" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = projectGtcSearchParamsSchema.parse(searchParams) - - const promises = Promise.all([ - getProjectGtcList({ - page: search.page, - perPage: search.perPage, - search: search.search, - sort: search.sort, - }), - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 프로젝트 GTC 관리 - </h2> - {/* <p className="text-muted-foreground"> - 프로젝트별 GTC(General Terms and Conditions) 파일을 관리할 수 있습니다. - 각 프로젝트마다 하나의 GTC 파일을 업로드할 수 있으며, 파일 업로드 시 기존 파일은 자동으로 교체됩니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* 추가 기능이 필요하면 여기에 추가 */} - </React.Suspense> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={8} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["3rem", "3rem", "12rem", "20rem", "10rem", "20rem", "15rem", "12rem", "3rem"]} - shrinkZero - /> - } - > - <ProjectGtcTable promises={promises} /> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/project-vendors/page.tsx b/app/[lng]/sales/(sales)/project-vendors/page.tsx deleted file mode 100644 index 525cff07..00000000 --- a/app/[lng]/sales/(sales)/project-vendors/page.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ProjectAVLTable } from "@/lib/project-avl/table/proejctAVL-table" -import { getProjecTAVL } from "@/lib/project-avl/service" -import { searchProjectAVLParamsCache } from "@/lib/project-avl/validations" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchProjectAVLParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjecTAVL({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 프로젝트 AVL 리스트 - </h2> - {/* <p className="text-muted-foreground"> - 프로젝트 PQ를 통과한 벤더의 리스트를 보여줍니다.{" "} - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <ProjectAVLTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/sales/(sales)/projects/page.tsx b/app/[lng]/sales/(sales)/projects/page.tsx deleted file mode 100644 index 649dd56f..00000000 --- a/app/[lng]/sales/(sales)/projects/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" -import { ItemsTable } from "@/lib/items/table/items-table" -import { getProjectLists } from "@/lib/projects/service" -import { ProjectsTable } from "@/lib/projects/table/projects-table" -import { searchParamsProjectsCache } from "@/lib/projects/validation" - - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsProjectsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getProjectLists({ - ...search, - filters: validFilters, - }), - - ]) - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 수행 프로젝트 리스트 from S-EDP - </h2> - {/* <p className="text-muted-foreground"> - S-EDP로부터 수신하는 프로젝트 리스트입니다. 향후 MDG로 전환됩니다.{" "} - <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. - </p> */} - </div> - </div> - </div> - - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} - </React.Suspense> - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <ProjectsTable promises={promises} /> - </React.Suspense> - </Shell> - ) -} diff --git a/app/[lng]/sales/(sales)/report/page.tsx b/app/[lng]/sales/(sales)/report/page.tsx deleted file mode 100644 index 152721cf..00000000 --- a/app/[lng]/sales/(sales)/report/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from "react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Shell } from "@/components/shell"; -import { ErrorBoundary } from "@/components/error-boundary"; -import { getDashboardData } from "@/lib/dashboard/service"; -import { DashboardClient } from "@/lib/dashboard/dashboard-client"; - -// 데이터 fetch 시 비동기 함수 호출 후 await 하므로 static-pre-render 과정에서 dynamic-server-error 발생. -// 따라서, dynamic 속성을 force-dynamic 으로 설정하여 동적 렌더링 처리 -// getDashboardData 함수에 대한 Promise를 넘기는 식으로 수정하게 되면 force-dynamic 선언을 제거해도 됨. -export const dynamic = 'force-dynamic' - -export default async function IndexPage() { - // domain을 명시적으로 전달 - const domain = "sales"; - - try { - // 서버에서 직접 데이터 fetch - const dashboardData = await getDashboardData(domain); - - return ( - <Shell className="gap-2"> - <DashboardClient initialData={dashboardData} /> - </Shell> - ); - } catch (error) { - console.error("Dashboard data fetch error:", error); - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-center py-12"> - <div className="text-center space-y-2"> - <p className="text-destructive">데이터를 불러오는데 실패했습니다.</p> - <p className="text-muted-foreground text-sm">{error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다."}</p> - </div> - </div> - </Shell> - ); - } -} - -function DashboardSkeleton() { - return ( - <div className="space-y-6"> - {/* 헤더 스켈레톤 */} - <div className="flex items-center justify-between"> - <div className="space-y-2"> - <Skeleton className="h-8 w-48" /> - <Skeleton className="h-4 w-72" /> - </div> - <Skeleton className="h-10 w-24" /> - </div> - - {/* 요약 카드 스켈레톤 */} - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> - {[...Array(4)].map((_, i) => ( - <div key={i} className="space-y-3 p-6 border rounded-lg"> - <div className="flex items-center justify-between"> - <Skeleton className="h-4 w-16" /> - <Skeleton className="h-4 w-4" /> - </div> - <Skeleton className="h-8 w-12" /> - <Skeleton className="h-3 w-20" /> - </div> - ))} - </div> - - {/* 차트 스켈레톤 */} - <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> - {[...Array(2)].map((_, i) => ( - <div key={i} className="space-y-4 p-6 border rounded-lg"> - <div className="space-y-2"> - <Skeleton className="h-6 w-32" /> - <Skeleton className="h-4 w-48" /> - </div> - <Skeleton className="h-[300px] w-full" /> - </div> - ))} - </div> - - {/* 탭 스켈레톤 */} - <div className="space-y-4"> - <Skeleton className="h-10 w-64" /> - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> - {[...Array(6)].map((_, i) => ( - <div key={i} className="space-y-4 p-6 border rounded-lg"> - <Skeleton className="h-6 w-32" /> - <div className="space-y-3"> - <div className="flex justify-between"> - <Skeleton className="h-4 w-16" /> - <Skeleton className="h-4 w-12" /> - </div> - <div className="flex gap-2"> - <Skeleton className="h-6 w-16" /> - <Skeleton className="h-6 w-16" /> - <Skeleton className="h-6 w-16" /> - </div> - <Skeleton className="h-2 w-full" /> - </div> - </div> - ))} - </div> - </div> - </div> - ); -} diff --git a/app/[lng]/sales/(sales)/settings/layout.tsx b/app/[lng]/sales/(sales)/settings/layout.tsx deleted file mode 100644 index 6c380919..00000000 --- a/app/[lng]/sales/(sales)/settings/layout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "Settings", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "Account", - href: `/${lng}/evcp/settings`, - }, - { - title: "Preferences", - href: `/${lng}/evcp/settings/preferences`, - } - - - ] - - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - <div className="space-y-0.5"> - <h2 className="text-2xl font-bold tracking-tight">설정</h2> - {/* <p className="text-muted-foreground"> - Manage your account settings and preferences. - </p> */} - </div> - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="-mx-4 lg:w-1/5"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="flex-1 ">{children}</div> - </div> - </div> - </section> - </div> - - - </> - ) -} diff --git a/app/[lng]/sales/(sales)/settings/page.tsx b/app/[lng]/sales/(sales)/settings/page.tsx deleted file mode 100644 index eba5e948..00000000 --- a/app/[lng]/sales/(sales)/settings/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AccountForm } from "@/components/settings/account-form" - -export default function SettingsAccountPage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Account</h3> - {/* <p className="text-sm text-muted-foreground"> - Update your account settings. Set your preferred language and - timezone. - </p> */} - </div> - <Separator /> - <AccountForm /> - </div> - ) -} diff --git a/app/[lng]/sales/(sales)/settings/preferences/page.tsx b/app/[lng]/sales/(sales)/settings/preferences/page.tsx deleted file mode 100644 index e2a88021..00000000 --- a/app/[lng]/sales/(sales)/settings/preferences/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { AppearanceForm } from "@/components/settings/appearance-form" - -export default function SettingsAppearancePage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Preference</h3> - <p className="text-sm text-muted-foreground"> - Customize the preference of the app. - </p> - </div> - <Separator /> - <AppearanceForm /> - </div> - ) -} diff --git a/app/[lng]/sales/(sales)/system/admin-users/page.tsx b/app/[lng]/sales/(sales)/system/admin-users/page.tsx deleted file mode 100644 index 11a9e9fb..00000000 --- a/app/[lng]/sales/(sales)/system/admin-users/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { DateRangePicker } from "@/components/date-range-picker" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllCompanies, getAllRoles, getUserCountGroupByCompany, getUserCountGroupByRole, getUsers } from "@/lib/admin-users/service" -import { AdmUserTable } from "@/lib/admin-users/table/ausers-table" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsers({ - ...search, - filters: validFilters, - }), - getUserCountGroupByCompany(), - getUserCountGroupByRole(), - getAllCompanies(), - getAllRoles() - ]) - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Vendor Admin User Management</h3> - <p className="text-sm text-muted-foreground"> - 협력업체의 유저 전체를 조회하고 어드민 유저를 생성할 수 있는 페이지입니다. 이곳에서 초기 유저를 생성시킬 수 있습니다. <br />생성 후에는 생성된 사용자의 이메일로 생성 통보 이메일이 발송되며 사용자는 이메일과 OTP로 로그인이 가능합니다. - </p> - </div> - <Separator /> - <AdmUserTable promises={promises} /> - </div> - </React.Suspense> - - ) -} diff --git a/app/[lng]/sales/(sales)/system/layout.tsx b/app/[lng]/sales/(sales)/system/layout.tsx deleted file mode 100644 index 2776ed8b..00000000 --- a/app/[lng]/sales/(sales)/system/layout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Metadata } from "next" - -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/layout/sidebar-nav" - -export const metadata: Metadata = { - title: "System Setting", - // description: "Advanced form example using react-hook-form and Zod.", -} - - -interface SettingsLayoutProps { - children: React.ReactNode - params: { lng: string } -} - -export default async function SettingsLayout({ - children, - params, -}: { - children: React.ReactNode - params: { lng: string } -}) { - const resolvedParams = await params - const lng = resolvedParams.lng - - - const sidebarNavItems = [ - - { - title: "삼성중공업 사용자", - href: `/${lng}/evcp/system`, - }, - { - title: "Roles", - href: `/${lng}/evcp/system/roles`, - }, - { - title: "권한 통제", - href: `/${lng}/evcp/system/permissions`, - }, - { - title: "협력업체 사용자", - href: `/${lng}/evcp/system/admin-users`, - }, - - { - title: "비밀번호 정책", - href: `/${lng}/evcp/system/password-policy`, - }, - - ] - - - return ( - <> - <div className="container py-6"> - <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> - <div className="hidden space-y-6 p-10 pb-16 md:block"> - <div className="space-y-0.5"> - <h2 className="text-2xl font-bold tracking-tight">시스템 설정</h2> - {/* <p className="text-muted-foreground"> - 사용자, 롤, 접근 권한을 관리하세요. - </p> */} - </div> - <Separator className="my-6" /> - <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="-mx-4 lg:w-1/5"> - <SidebarNav items={sidebarNavItems} /> - </aside> - <div className="flex-1 ">{children}</div> - </div> - </div> - </section> - </div> - - - </> - ) -} diff --git a/app/[lng]/sales/(sales)/system/page.tsx b/app/[lng]/sales/(sales)/system/page.tsx deleted file mode 100644 index fe0a262c..00000000 --- a/app/[lng]/sales/(sales)/system/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Separator } from "@/components/ui/separator" -import { type SearchParams } from "@/types/table" -import * as React from "react" -import { getValidFilters } from "@/lib/data-table" -import { searchParamsCache } from "@/lib/admin-users/validations" -import { getAllRoles, getUsersEVCP } from "@/lib/users/service" -import { getUserCountGroupByRole } from "@/lib/admin-users/service" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { UserTable } from "@/lib/users/table/users-table" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function SystemUserPage(props: IndexPageProps) { - - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getUsersEVCP({ - ...search, - filters: validFilters, - }), - getUserCountGroupByRole(), - getAllRoles() - ]) - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "12rem", "12rem", "12rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">SHI Users</h3> - <p className="text-sm text-muted-foreground"> - 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다. - </p> - </div> - <Separator /> - <UserTable promises={promises} /> - </div> - </React.Suspense> - - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/system/password-policy/page.tsx b/app/[lng]/sales/(sales)/system/password-policy/page.tsx deleted file mode 100644 index 0f14fefe..00000000 --- a/app/[lng]/sales/(sales)/system/password-policy/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// app/admin/password-policy/page.tsx - -import * as React from "react" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { AlertTriangle } from "lucide-react" -import SecuritySettingsTable from "@/components/system/passwordPolicy" -import { getSecuritySettings } from "@/lib/password-policy/service" - - -export default async function PasswordPolicyPage() { - try { - // 보안 설정 데이터 로드 - const securitySettings = await getSecuritySettings() - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={4} - searchableColumnCount={0} - filterableColumnCount={0} - cellWidths={["20rem", "30rem", "15rem", "10rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3> - <p className="text-sm text-muted-foreground"> - 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. - </p> - </div> - <Separator /> - <SecuritySettingsTable initialSettings={securitySettings} /> - </div> - </React.Suspense> - ) - } catch (error) { - console.error('Failed to load security settings:', error) - - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">협력업체 사용자 비밀번호 정책 설정</h3> - <p className="text-sm text-muted-foreground"> - 협력업체 사용자들을 위한 비밀번호 정책과 보안 설정을 관리할 수 있습니다. - </p> - </div> - <Separator /> - <Alert variant="destructive"> - <AlertTriangle className="h-4 w-4" /> - <AlertDescription> - 보안 설정을 불러오는 중 오류가 발생했습니다. 페이지를 새로고침하거나 관리자에게 문의하세요. - </AlertDescription> - </Alert> - </div> - ) - } -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/system/permissions/page.tsx b/app/[lng]/sales/(sales)/system/permissions/page.tsx deleted file mode 100644 index 6aa2b693..00000000 --- a/app/[lng]/sales/(sales)/system/permissions/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import PermissionsTree from "@/components/system/permissionsTree" -import { Separator } from "@/components/ui/separator" - -export default function PermissionsPage() { - return ( - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Permissions</h3> - <p className="text-sm text-muted-foreground"> - Set permissions to the menu by Role - </p> - </div> - <Separator /> - <PermissionsTree/> - </div> - ) -} diff --git a/app/[lng]/sales/(sales)/system/roles/page.tsx b/app/[lng]/sales/(sales)/system/roles/page.tsx deleted file mode 100644 index fe074600..00000000 --- a/app/[lng]/sales/(sales)/system/roles/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Separator } from "@/components/ui/separator" - -import { searchParamsCache } from "@/lib/roles/validations" -import { searchParamsCache as searchParamsCache2 } from "@/lib/admin-users/validations" -import { RolesTable } from "@/lib/roles/table/roles-table" -import { getRolesWithCount } from "@/lib/roles/services" -import { getUsersAll } from "@/lib/users/service" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function UserTable(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCache.parse(searchParams) - const search2 = searchParamsCache2.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getRolesWithCount({ - ...search, - filters: validFilters, - }), - - - ]) - - - const promises2 = Promise.all([ - getUsersAll({ - ...search2, - filters: validFilters, - }, "evcp"), - ]) - - - return ( - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <div className="space-y-6"> - <div> - <h3 className="text-lg font-medium">Role Management</h3> - <p className="text-sm text-muted-foreground"> - 역할을 생성하고 역할에 유저를 할당할 수 있는 페이지입니다. 역할에 메뉴의 접근 권한 역시 할당할 수 있습니다. - </p> - </div> - <Separator /> - <RolesTable promises={promises} promises2={promises2} /> - </div> - </React.Suspense> - - ) -} diff --git a/app/[lng]/sales/(sales)/tbe/page.tsx b/app/[lng]/sales/(sales)/tbe/page.tsx deleted file mode 100644 index 211cf376..00000000 --- a/app/[lng]/sales/(sales)/tbe/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" -import { getAllTBE } from "@/lib/rfqs/service" -import { searchParamsTBECache } from "@/lib/rfqs/validations" -import { AllTbeTable } from "@/lib/tbe/table/tbe-table" -import { RfqType } from "@/lib/rfqs/validations" -import * as React from "react" -import { Shell } from "@/components/shell" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" - -interface IndexPageProps { - params: { - lng: string - } - searchParams: Promise<SearchParams> -} - -// 타입별 페이지 설명 구성 (Budgetary 제외) -const typeConfig: Record<string, { title: string; description: string; rfqType: RfqType }> = { - "purchase": { - title: "Purchase RFQ Technical Bid Evaluation", - description: "실제 구매 발주 전 가격 요청을 위한 TBE입니다.", - rfqType: RfqType.PURCHASE - }, - "purchase-budgetary": { - title: "Purchase Budgetary RFQ Technical Bid Evaluation", - description: "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 TBE입니다.", - rfqType: RfqType.PURCHASE_BUDGETARY - } -} - -export default async function RfqTBEPage(props: IndexPageProps) { - const resolvedParams = await props.params - const lng = resolvedParams.lng - - // URL 쿼리 파라미터에서 타입 추출 - const searchParams = await props.searchParams - // 기본값으로 'purchase' 사용 - const typeParam = searchParams?.type as string || 'purchase' - - // 유효한 타입인지 확인하고 기본값 설정 - const validType = Object.keys(typeConfig).includes(typeParam) ? typeParam : 'purchase' - const rfqType = typeConfig[validType].rfqType - - // SearchParams 파싱 (Zod) - const search = searchParamsTBECache.parse(searchParams) - const validFilters = getValidFilters(search.filters) - - // 현재 선택된 타입의 데이터 로드 - const promises = Promise.all([ - getAllTBE({ - ...search, - filters: validFilters, - rfqType - }) - ]) - - // 페이지 경로 생성 함수 - 단순화 - const getTabUrl = (type: string) => { - return `/${lng}/evcp/tbe?type=${type}`; - } - - return ( - <Shell className="gap-2"> - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - TBE 관리 - </h2> - {/* <p className="text-muted-foreground"> - 초대된 협력업체에게 TBE를 보낼 수 있습니다. <br/> - 체크박스 선택을 하면 초대 버튼이 활성화됩니다. 버튼 클릭 후 첨부파일을 함께 전송하면 TBE 내용이 메일로 전달되고 eVCP에도 협력업체가 입력할 수 있게 자동 생성됩니다. - </p> */} - </div> - </div> - </div> - - {/* 타입 선택 탭 (Budgetary 제외) */} - <Tabs defaultValue={validType} value={validType} className="w-full"> - <TabsList className="grid grid-cols-2 w-full max-w-md"> - <TabsTrigger value="purchase" asChild> - <a href={getTabUrl('purchase')}>Purchase</a> - </TabsTrigger> - <TabsTrigger value="purchase-budgetary" asChild> - <a href={getTabUrl('purchase-budgetary')}>Purchase Budgetary</a> - </TabsTrigger> - </TabsList> - - <div className="mt-2"> - <p className="text-sm text-muted-foreground"> - {typeConfig[validType].description} - </p> - </div> - </Tabs> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <AllTbeTable promises={promises}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx b/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx deleted file mode 100644 index 5bc36790..00000000 --- a/app/[lng]/sales/(sales)/tech-contact-possible-items/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Suspense } from "react"
-import { SearchParams } from "@/types/table"
-import { Shell } from "@/components/shell"
-import { ContactPossibleItemsTable } from "@/lib/contact-possible-items/table/contact-possible-items-table"
-import { getContactPossibleItems } from "@/lib/contact-possible-items/service"
-import { searchParamsCache } from "@/lib/contact-possible-items/validations"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-
-
-interface ContactPossibleItemsPageProps {
- searchParams: Promise<SearchParams>
-}
-
-export default async function ContactPossibleItemsPage({
- searchParams,
-}: ContactPossibleItemsPageProps) {
- const resolvedSearchParams = await searchParams
- const search = searchParamsCache.parse(resolvedSearchParams)
-
- const contactPossibleItemsPromise = getContactPossibleItems(search)
-
- return (
- <Shell className="gap-2">
- <div className="flex items-center justify-between space-y-2">
- <div className="flex items-center justify-between space-y-2">
- <div>
- <h2 className="text-2xl font-bold tracking-tight">
- 담당자별 자재 관리
- </h2>
- {/* <p className="text-muted-foreground">
- 기술영업 담당자별 자재를 관리합니다.
- </p> */}
- </div>
- </div>
- </div>
-
-
- <Suspense
- fallback={
- <DataTableSkeleton
- columnCount={12}
- searchableColumnCount={2}
- filterableColumnCount={3}
- cellWidths={["10rem", "10rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <ContactPossibleItemsTable
- contactPossibleItemsPromise={contactPossibleItemsPromise}
- />
- </Suspense>
-
- </Shell>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-project-avl/page.tsx b/app/[lng]/sales/(sales)/tech-project-avl/page.tsx deleted file mode 100644 index 4ce018cd..00000000 --- a/app/[lng]/sales/(sales)/tech-project-avl/page.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import * as React from "react"
-import { redirect } from "next/navigation"
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/app/api/auth/[...nextauth]/route"
-import { SearchParams } from "@/types/table"
-import { searchParamsCache } from "@/lib/tech-project-avl/validations"
-import { Skeleton } from "@/components/ui/skeleton"
-import { Shell } from "@/components/shell"
-import { AcceptedQuotationsTable } from "@/lib/tech-project-avl/table/accepted-quotations-table"
-import { getAcceptedTechSalesVendorQuotations } from "@/lib/techsales-rfq/service"
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { Ellipsis } from "lucide-react"
-import { InformationButton } from "@/components/information/information-button"
-export interface PageProps {
- params: Promise<{ lng: string }>
- searchParams: Promise<SearchParams>
-}
-
-export default async function AcceptedQuotationsPage({
- params,
- searchParams,
-}: PageProps) {
- const { lng } = await params
-
- const session = await getServerSession(authOptions)
- if (!session) {
- redirect(`/${lng}/auth/signin`)
- }
-
- const search = await searchParams
- const { page, perPage, sort, filters, search: searchText } = searchParamsCache.parse(search)
- const validFilters = getValidFilters(filters ?? [])
-
- const { data, pageCount } = await getAcceptedTechSalesVendorQuotations({
- page,
- perPage: perPage ?? 10,
- sort,
- search: searchText,
- filters: validFilters,
- })
-
- return (
- <Shell className="gap-2">
- <div className="flex items-center justify-between space-y-2">
- <div className="flex items-center justify-between space-y-2">
- <div>
- <div className="flex items-center gap-2">
- <h2 className="text-2xl font-bold tracking-tight">
- 견적 Result 전송
- </h2>
- <InformationButton pagePath="evcp/tech-project-avl" />
- </div>
- {/* <p className="text-muted-foreground">
- 기술영업 승인 견적서에 대한 요약 정보를 확인하고{" "}
- <span className="inline-flex items-center whitespace-nowrap">
- <Ellipsis className="size-3" />
- <span className="ml-1">버튼</span>
- </span>
- 을 통해 RFQ 코드, 설명, 업체명, 업체 코드 등의 상세 정보를 확인할 수 있습니다.
- </p> */}
- </div>
- </div>
- </div>
-
- <React.Suspense fallback={<Skeleton className="h-7 w-52" />}>
- {/* Date range picker can be added here if needed */}
- </React.Suspense>
-
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={12}
- searchableColumnCount={2}
- filterableColumnCount={4}
- cellWidths={["10rem", "15rem", "12rem", "10rem", "10rem", "12rem", "8rem", "12rem", "10rem", "8rem", "10rem", "10rem"]}
- shrinkZero
- />
- }
- >
- <AcceptedQuotationsTable
- data={data}
- pageCount={pageCount}
- />
- </React.Suspense>
- </Shell>
- )
-}
diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx deleted file mode 100644 index 291cd630..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/layout.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Metadata } from "next"
-
-import { Separator } from "@/components/ui/separator"
-import { SidebarNav } from "@/components/layout/sidebar-nav"
-import { findTechVendorById } from "@/lib/tech-vendors/service"
-import { TechVendor } from "@/db/schema/techVendors"
-import { Button } from "@/components/ui/button"
-import { ArrowLeft } from "lucide-react"
-import Link from "next/link"
-export const metadata: Metadata = {
- title: "Tech Vendor Detail",
-}
-
-export default async function SettingsLayout({
- children,
- params,
-}: {
- children: React.ReactNode
- params: { lng: string , id: string}
-}) {
-
- // 1) URL 파라미터에서 id 추출, Number로 변환
- const resolvedParams = await params
- const lng = resolvedParams.lng
- const id = resolvedParams.id
-
- const idAsNumber = Number(id)
- // 2) DB에서 해당 협력업체 정보 조회
- const vendor: TechVendor | null = await findTechVendorById(idAsNumber)
-
- // 3) 사이드바 메뉴
- const sidebarNavItems = [
- {
- title: "연락처",
- href: `/${lng}/evcp/tech-vendors/${id}/info`,
- },
- {
- title: "RFQ 히스토리",
- href: `/${lng}/evcp/tech-vendors/${id}/info/rfq-history`,
- },
- {
- title: "자재 리스트",
- href: `/${lng}/evcp/tech-vendors/${id}/info/possible-items`,
- },
- ]
-
- return (
- <>
- <div className="container py-6">
- <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
- <div className="hidden space-y-6 p-10 pb-16 md:block">
- {/* RFQ 목록으로 돌아가는 링크 추가 */}
- <div className="flex items-center justify-end mb-4">
- <Link href={`/${lng}/evcp/tech-vendors`} passHref>
- <Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto">
- <ArrowLeft className="mr-1 h-4 w-4" />
- <span>기술영업 벤더 목록으로 돌아가기</span>
- </Button>
- </Link>
- </div>
- <div className="space-y-0.5">
- {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */}
- <h2 className="text-2xl font-bold tracking-tight">
- {vendor
- ? `${vendor.vendorCode ?? ""} - ${vendor.vendorName} 상세 정보`
- : "Loading Vendor..."}
- </h2>
- <p className="text-muted-foreground">기술영업 벤더 관련 상세사항을 확인하세요.</p>
- </div>
- <Separator className="my-6" />
- <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
- <aside className="-mx-4 lg:w-1/5">
- <SidebarNav items={sidebarNavItems} />
- </aside>
- <div className="flex-1">{children}</div>
- </div>
- </div>
- </section>
- </div>
- </>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx deleted file mode 100644 index 9969a801..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Separator } from "@/components/ui/separator"
-import { getTechVendorContacts } from "@/lib/tech-vendors/service"
-import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { searchParamsContactCache } from "@/lib/tech-vendors/validations"
-import { TechVendorContactsTable } from "@/lib/tech-vendors/contacts-table/contact-table"
-
-interface IndexPageProps {
- // Next.js 13 App Router에서 기본으로 주어지는 객체들
- params: {
- lng: string
- id: string
- }
- searchParams: Promise<SearchParams>
-}
-
-export default async function SettingsAccountPage(props: IndexPageProps) {
- const resolvedParams = await props.params
- const id = resolvedParams.id
-
- const idAsNumber = Number(id)
-
- // 2) SearchParams 파싱 (Zod)
- // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼
- const searchParams = await props.searchParams
- const search = searchParamsContactCache.parse(searchParams)
- const validFilters = getValidFilters(search.filters)
-
-
-
- const promises = Promise.all([
- getTechVendorContacts({
- ...search,
- filters: validFilters,
- },
- idAsNumber)
- ])
- // 4) 렌더링
- return (
- <div className="space-y-6">
- <div>
- <h3 className="text-lg font-medium">
- Contacts
- </h3>
- <p className="text-sm text-muted-foreground">
- 업무별 담당자 정보를 확인하세요.
- </p>
- </div>
- <Separator />
- <div>
- <TechVendorContactsTable promises={promises} vendorId={idAsNumber}/>
- </div>
- </div>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx deleted file mode 100644 index 642c6e32..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/possible-items/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Separator } from "@/components/ui/separator"
-import { getTechVendorPossibleItems } from "@/lib/tech-vendors/service"
-import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { searchParamsPossibleItemsCache } from "@/lib/tech-vendors/validations"
-import { TechVendorPossibleItemsTable } from "@/lib/tech-vendors/possible-items/possible-items-table"
-
-interface IndexPageProps {
- // Next.js 13 App Router에서 기본으로 주어지는 객체들
- params: Promise<{
- lng: string
- id: string
- }>
- searchParams: Promise<SearchParams>
-}
-
-export default async function TechVendorPossibleItemsPage(props: IndexPageProps) {
- const resolvedParams = await props.params
- const id = resolvedParams.id
-
- const idAsNumber = Number(id)
- console.log(idAsNumber)
-
- // 2) SearchParams 파싱 (Zod)
- // - "filters", "page", "perPage", "sort" 등 possible items 전용 컬럼
- const searchParams = await props.searchParams
- const search = searchParamsPossibleItemsCache.parse(searchParams)
- const validFilters = getValidFilters(search.filters)
-
- const promises = Promise.all([
- getTechVendorPossibleItems({
- ...search,
- filters: validFilters,
- }, idAsNumber)
- ])
-
- // 4) 렌더링
- return (
- <div className="space-y-6">
- <div>
- <h3 className="text-lg font-medium">
- 공급가능 아이템 목록
- </h3>
- <p className="text-sm text-muted-foreground">
- 해당 벤더가 공급 가능한 아이템 목록을 확인할 수 있습니다.
- </p>
- </div>
- <Separator />
- <div>
- <TechVendorPossibleItemsTable promises={promises} vendorId={idAsNumber} />
- </div>
- </div>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx deleted file mode 100644 index 9122d524..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/[id]/info/rfq-history/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { type SearchParams } from "@/types/table"
-import { getValidFilters } from "@/lib/data-table"
-import { TechVendorRfqHistoryTable } from "@/lib/tech-vendors/rfq-history-table/tech-vendor-rfq-history-table"
-import { getTechVendorRfqHistory } from "@/lib/tech-vendors/service"
-import { searchParamsRfqHistoryCache } from "@/lib/tech-vendors/validations"
-import { Separator } from "@/components/ui/separator"
-
-interface IndexPageProps {
- // Next.js 13 App Router에서 기본으로 주어지는 객체들
- params: {
- lng: string
- id: string
- }
- searchParams: Promise<SearchParams>
-}
-
-export default async function SettingsAccountPage(props: IndexPageProps) {
- const resolvedParams = await props.params
- const id = resolvedParams.id
-
- const idAsNumber = Number(id)
-
- // 2) SearchParams 파싱 (Zod)
- // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼
- const searchParams = await props.searchParams
- const search = searchParamsRfqHistoryCache.parse(searchParams)
- const validFilters = getValidFilters(search.filters)
-
-
-
- const promises = Promise.all([
- getTechVendorRfqHistory({
- ...search,
- filters: validFilters,
- },
- idAsNumber)
- ])
-
- return (
- <div className="space-y-6">
- <div>
- <h3 className="text-lg font-medium">
- RFQ 히스토리
- </h3>
- <p className="text-sm text-muted-foreground">
- 벤더가 참여한 기술영업 RFQ 목록입니다.
- </p>
- </div>
- <Separator />
- <div>
- <TechVendorRfqHistoryTable promises={promises} />
- </div>
- </div>
-
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/tech-vendors/page.tsx b/app/[lng]/sales/(sales)/tech-vendors/page.tsx deleted file mode 100644 index e49ba79e..00000000 --- a/app/[lng]/sales/(sales)/tech-vendors/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react"
-import { type SearchParams } from "@/types/table"
-
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { Shell } from "@/components/shell"
-
-import { searchParamsCache } from "@/lib/tech-vendors/validations"
-import { getTechVendors, getTechVendorStatusCounts } from "@/lib/tech-vendors/service"
-import { TechVendorsTable } from "@/lib/tech-vendors/table/tech-vendors-table"
-
-interface IndexPageProps {
- searchParams: Promise<SearchParams>
-}
-
-export default async function IndexPage(props: IndexPageProps) {
- const searchParams = await props.searchParams
- const search = searchParamsCache.parse(searchParams)
-
- const validFilters = getValidFilters(search.filters)
-
- const promises = Promise.all([
- getTechVendors({
- ...search,
- filters: validFilters,
- }),
- getTechVendorStatusCounts(),
- ])
-
- return (
- <Shell className="gap-4">
- <div className="flex items-center justify-between">
- {/* 왼쪽: 타이틀 & 설명 */}
- <div>
- <div className="flex items-center gap-2">
- <h2 className="text-2xl font-bold tracking-tight">기술영업 협력업체 관리</h2>
- {/* InformationButton은 필요시 추가 */}
- {/* <InformationButton pagePath="evcp/tech-vendors" /> */}
- </div>
- {/* <p className="text-muted-foreground">
- 기술영업 벤더에 대한 요약 정보를 확인하고 관리할 수 있습니다.
- </p> */}
- </div>
- </div>
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={6}
- searchableColumnCount={1}
- filterableColumnCount={2}
- cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]}
- shrinkZero
- />
- }
- >
- <TechVendorsTable promises={promises} />
- </React.Suspense>
- </Shell>
- )
-}
\ No newline at end of file diff --git a/app/[lng]/sales/(sales)/vendor-candidates/page.tsx b/app/[lng]/sales/(sales)/vendor-candidates/page.tsx deleted file mode 100644 index f4bee95b..00000000 --- a/app/[lng]/sales/(sales)/vendor-candidates/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react" -import { type SearchParams } from "@/types/table" - -import { getValidFilters } from "@/lib/data-table" -import { Skeleton } from "@/components/ui/skeleton" -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" -import { Shell } from "@/components/shell" - -import { getVendorCandidateCounts, getVendorCandidates } from "@/lib/vendor-candidates/service" -import { searchParamsCandidateCache } from "@/lib/vendor-candidates/validations" -import { VendorCandidateTable } from "@/lib/vendor-candidates/table/candidates-table" -import { DateRangePicker } from "@/components/date-range-picker" - -interface IndexPageProps { - searchParams: Promise<SearchParams> -} - -export default async function IndexPage(props: IndexPageProps) { - const searchParams = await props.searchParams - const search = searchParamsCandidateCache.parse(searchParams) - - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getVendorCandidates({ - ...search, - filters: validFilters, - }), - getVendorCandidateCounts() - ]) - - return ( - <Shell className="gap-2"> - - <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <h2 className="text-2xl font-bold tracking-tight"> - 발굴업체 등록 관리 - </h2> - {/* <p className="text-muted-foreground"> - 수집한 협력업체 후보를 등록하고 초대 메일을 송부할 수 있습니다. - </p> */} - </div> - </div> - </div> - - {/* 수집일 라벨과 DateRangePicker를 함께 배치 */} - <div className="flex items-center justify-start gap-2"> - {/* <span className="text-sm font-medium">수집일 기간 설정: </span> */} - <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - <DateRangePicker - triggerSize="sm" - triggerClassName="w-56 sm:w-60" - align="end" - shallow={false} - showClearButton={true} - placeholder="수집일 날짜 범위를 고르세요" - /> - </React.Suspense> - </div> - - <React.Suspense - fallback={ - <DataTableSkeleton - columnCount={6} - searchableColumnCount={1} - filterableColumnCount={2} - cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} - shrinkZero - /> - } - > - <VendorCandidateTable promises={promises}/> - </React.Suspense> - </Shell> - ) -}
\ No newline at end of file diff --git a/app/[lng]/sales/page.tsx b/app/[lng]/sales/page.tsx deleted file mode 100644 index f9662cb7..00000000 --- a/app/[lng]/sales/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Metadata } from "next" -import { Suspense } from "react" -import { LoginFormSkeleton } from "@/components/login/login-form-skeleton" -import { LoginFormSHI } from "@/components/login/login-form-shi" - -export const metadata: Metadata = { - title: "eVCP Portal", - description: "", -} - -export default function AuthenticationPage() { - - - return ( - <> - <Suspense fallback={<LoginFormSkeleton/>}> - <LoginFormSHI /> - </Suspense> - </> - ) -} |
